Skip to content

语言模型演进:N-gram 到 RNN ​

标签
AI/llm
字数
4765 字
阅读时间
19 分钟

语言模型(Language Model, LM)的任务是计算一个词序列出现的概率。这条演进线要解决的问题始终是同一个:上下文能看多长,以及没见过的情况怎么办。

N-gram 与马尔可夫假设 ​

一个句子的概率按概率的链式法则展开:

P(S)=P(w1)⋅P(w2∣w1)⋅P(w3∣w1,w2)⋯P(wm∣w1,…,wm−1)

直接算不现实:像 P(wm∣w1,…,wm−1) 这样的条件概率,词序列 w1,…,wm−1 可能从未在语料里出现过。

马尔可夫假设的作用是把它砍短——一个词的出现只与前面有限的 n−1 个词有关:

  • Bigram(N=2):P(wi∣w1,…,wi−1)≈P(wi∣wi−1)
  • Trigram(N=3):P(wi∣w1,…,wi−1)≈P(wi∣wi−2,wi−1)

概率用**最大似然估计(MLE)**从语料里数出来,思想就是「数据里出现次数最多的最可能」:

P(wi∣wi−1)=Count(wi−1,wi)Count(wi−1)

手算例子。语料 datawhale agent learns, datawhale agent works(共 6 个词),估 datawhale agent learns 的 Bigram 概率:

步骤计算结果
P(datawhale)2/60.333
P(agent∣datawhale)Count(datawhale agent)/Count(datawhale)=2/21.000
P(learns∣agent)Count(agent learns)/Count(agent)=1/20.500
连乘0.333×1×0.50.167

两个致命缺陷 ​

  1. 数据稀疏性(Sparsity):词序列没在语料里出现过,概率估计就是 0。平滑(Smoothing)能缓解,但根除不了。
  2. 泛化能力差:模型不理解词与词之间的语义相似性。见过很多次 agent learns,也泛化不到 robot learns——如果 robot 或 robot learns 没出现过,概率同样是 0,因为模型不知道 agent 和 robot 语义相近。

根因是 N-gram 把词当成孤立、离散的符号——它统计的是离散符号之间的条件概率,即使两个词语义极近也无法在同一上下文里互相替代,这在相当程度上加剧了数据稀疏。

平滑的两条路线 ​

方法做法
拉普拉斯加一最简单的做法——给所有计数都加 1,保证没有零概率。代价是把大量概率质量分给了从未出现过的组合
Kneser-Ney 平滑改进型回退平滑,用接续概率(一个词作为「延续」出现的可能性)而非原始频次来估计低阶分布。至今仍是 N-gram 的强基线

「平滑」这个方向的本质是承认一件事:语料的观测计数不可靠,需要给未观测事件留概率质量。后来的神经语言模型换了一条路——不修计数,而是把词变成连续向量。

语言模型的经典评价指标:困惑度 ​

在讲怎么改进之前,得先说清「怎么衡量好坏」。困惑度(Perplexity, PPL) 是这一层最标准的指标。

定义 ​

PP(W)=P(w1,w2,…,wN)−1N=2H(W)H(W)=−1N∑i=1Nlog2⁡P(wi∣w1,…,wi−1)

困惑度就是交叉熵的指数(用自然对数时 PP=eL;只要对数和指数用同一个底,数值不变)。

等价的几何平均形式更直观:

PP=(∏i1pθ(xi∣x<i))1N

怎么解读 ​

情形困惑度
完美预测1
给每个正确 token 都分配 0.25 的概率4(因为 −log⁡0.25=log⁡4)
在 V 个 token 上均匀预测V

「有效选择数」这个说法要当直觉用,别当定义:困惑度 4 不意味着每个位置都恰好有 4 个等概率候选——真实分布在不同位置差异很大,困惑度把它们平均掉了。

量级参照(这条最有价值) ​

模型 / 基准困惑度
Elman RNN(1990),PTB55
LSTM(2002),PTB43
Bigram(2 万词表)~200
Trigram~100–150
现代神经语言模型,PTB< 20
GPT-2,WikiText-10318.3
GPT-2,BooksCorpus20.2
GPT-3,BooksCorpus9.8
中文文本(一般范围)50–1000

这张表就是「神经模型相对统计模型」的量级差:PTB 上从 55 降到 20 以下。

另一条参照是理论上限:香农估计英语的熵率约 1.0–1.3 bits/字符,对应理论最低字符级困惑度约 2–2.5。实际模型的困惑度必然高于语言的真熵,因为模型只是不完美的近似。

困惑度的数值要放在同一坐标系里看才有意义。同一个测试集上跨代际的量级差:

困惑度(箭头右侧是配套的模型与基准;条形长度按下表数值取对数,只用于看量级)

 ≈200 ┤ ████████████████████  Bigram(V = 2 万)
≈100–150┤ ███████████████      Trigram
   55 ┤ ██████                Elman RNN,PTB(1990)
   43 ┤ ████                  LSTM,PTB(2002)
  <20 ┤ ██                    现代神经语言模型,PTB
 18.3 ┤ ██                    GPT-2,WikiText-103
  9.8 ┤ █                     GPT-3,BooksCorpus
     │
  1.0 ┼─── 完美预测
     │
     └──── 理论下界:香农给出的英语熵率约 1.0–1.3 bits/字符,
            对应字符级困惑度约 2–2.5。实际模型必然高于它 —— 模型只是真熵的不完美近似

另外三个锚点:给正确 token 各分 0.25 概率 → 4;在 V 个 token 上均匀预测 → V

比较的前提:词表必须一致 ​

只有在相同的词表和测试集下比较困惑度才有意义。

原因直白:不同词表意味着不同的有效分支因子。V=50000 的词表和一个 V=150000 的词表,「均匀预测」时的困惑度本来就差 3 倍——这两个数字根本不在同一个坐标系里。

当词表确实不同时,用 bits per byte 或 bits per character 做归一化更可比。

计算困惑度时必须对齐的六件事 ​

这部分很实用——同样的模型和测试集,预处理不同就能得到不同的困惑度:

项要求
Tokenizer不同 tokenizer 把同一文本切成不同目标,token 级困惑度不可直接比较
语料与预处理文档边界、归一化、end-of-text 标记、包含哪些文本,都会影响结果
上下文策略窗口长度、stride、上下文是否在文档或块边界重置
损失掩膜padding 和刻意排除的 prompt 位置不应计入损失,也不应计入 token 数
模型模式dropout 必须关闭
采样设置必须用模型的原始下一个 token 概率,不能经过采样、top-k 过滤或温度改动(见 04-采样参数)

另外要区分训练困惑度和验证困惑度:训练困惑度会随着模型记住训练样本而一直下降;验证困惑度测的是对未见文本的泛化,通常是更有信息量的那个数。

困惑度的边界 ​

它不直接测量事实准确性、推理、指令遵循、安全性或完整生成回答的质量。

  • 模型可以对常见文本模式给出高似然,同时给出错误答案
  • 指令调优的模型可以变得更有用,而在无关的预训练语料上困惑度并不改善——这是内在指标与外在表现的经典脱节
  • 困惑度隐藏了平均值内部的差异:少数领域或 token 类型可能有极高的损失,而总体数字看着还算合理 → 要配合逐 token 损失和分领域结果一起看

这也解释了为什么今天谈模型能力不再用困惑度:它是「训练曲线和同条件消融」的好指标,但选型要配合任务特定检查(见 08-模型选型 与 LLM Evaluation 与反馈闭环)。

神经网络语言模型与词嵌入 ​

2003 年 Bengio 等人提出的前馈神经网络语言模型是这条线的里程碑(Bengio et al., A neural probabilistic language model, JMLR 2003)。它的建模公式和 N-gram 一样(仍然只看前 n−1 个词),但输入换了表示:

p(s)=∏i=1np(wi∣wi−n+1…wi−1)

结构分三层:

层做什么
输入层通过词嵌入矩阵 C∈R|V|×m 取出前 n−1 个词各自的向量(m 是嵌入维度,每一行就是一个词的向量),再把它们拼接成上下文表示 x∈Rm(n−1)
隐藏层非线性变换
输出层输出下一个词的概率分布

词嵌入是这个架构解决稀疏性的关键:语义相近的词,向量位置也相近(agent 与 robot 靠得近,agent 与 apple 离得远)——于是即使 robot learns 从未出现,模型也能从 agent learns 泛化过去。

词嵌入是训练过程中自动学出来的 —— 在「预测下一个词」这个任务里,模型为了降低预测误差会不断调整每个词的向量位置,最终让向量蕴含语义。

衡量向量关系用余弦相似度:

similarity(a→,b→)=cos⁡θ=a→⋅b→|a→||b→|

方向相同 → 余弦 1(完全相关);正交 → 0(毫无关系);相反 → −1(完全负相关)。

词向量能捕捉抽象关系,经典例子:

vector(King)−vector(Man)+vector(Woman)≈vector(Queen)

相当于在语义空间里做平移:「国王」减去「男性」加上「女性」落到「女王」的位置。

从 Bengio 到 Word2Vec ​

Bengio 的做法把「学词向量」当作语言模型的副产品——训练成本高。后续工作把它拆出来当成独立目标:

方法思路
Word2Vec(Mikolov et al., 2013)两个变体:CBOW(用上下文词预测中心词)和 Skip-gram(用中心词预测上下文词)。用负采样或层次 softmax 把训练成本压下来——不再需要对整个词表做归一化
GloVe走全局共现统计路线:先统计整个语料的词共现矩阵,再对它做矩阵分解——把「局部窗口预测」和「全局统计」结合起来
FastText在词向量之上额外学子词(字符 n-gram)向量,把词表示成子词向量之和 → 能处理未登录词:即使一个词整体没见过,它的子词片段见过

这三条路线的分野很清晰:Word2Vec 是局部窗口 + 预测,GloVe 是全局共现 + 分解,FastText 是在词的基础上加子词结构。这个分层在 BPE 那里也能看到回声——子词是解决未登录词的通用手段。

共同的局限:上下文窗口仍然是固定的(只能看前 n−1 个词),和 N-gram 一样;而且一个词只有一个静态向量——apple 在「吃苹果」和「苹果公司」里是同一个向量。

Bengio 2003 的前馈网络语言模型:窗口仍是前 n−1 个词,换掉的是词的表示。

前 n−1 个词的 id
   │
   ▼
词嵌入矩阵 C ∈ R^{|V| × m} ── 按 id 取行,每一行就是一个词的向量
   │
   ▼
拼接(n = 3 时把 2 个 m 维向量首尾接成 2m 维)
   │
   ▼
隐藏层:非线性变换
   │
   ▼
输出层 ──▶ 下一个词在整词表上的分布

泛化能力从第一条边开始:连续向量让语义相近的词在空间里相邻,所以 robot learns 没在语料里出现过,也能从 agent learns 迁移过去。

Bengio 把学词向量当作语言模型的副产品,训练成本高;后续三条路线把它拆出来当成独立目标。

Word2Vec ── 局部窗口 + 预测 ─┬─ CBOW:用上下文词预测中心词
                             └─ Skip-gram:用中心词预测上下文词
                             成本靠负采样或层次 softmax 压下来
GloVe    ── 全局共现 + 分解 ── 先统计整个语料的词共现矩阵,再对它做矩阵分解
FastText ── 词 + 子词结构   ── 额外学子词(字符 n-gram)向量,词表示成子词向量之和
                               未登录词也能表示:整体没见过,它的片段见过

共同的局限:窗口仍然固定(只看前 n−1 个词),而且一个词只有一个静态向量
            —— 「吃苹果」和「苹果公司」里的 apple 是同一个向量

RNN 与 LSTM ​

RNN 打破了固定窗口:引入**隐藏状态(hidden state)**向量充当短期记忆,每一步读入当前词、结合上一刻的隐藏状态,生成新的隐藏状态传给下一刻。信息因此在序列里不断向后传递。

这里解决了两个 N-gram 的老问题:窗口不再固定(理论上能看到序列开头),参数也不再随窗口线性增长(同一组权重在每一步复用)。

长期依赖问题:RNN 的序列长度就是网络深度,梯度反向传播要经过多次连乘,会快速趋向零(梯度消失)或变得极大(梯度爆炸)。梯度消失使模型学不到序列早期信息对后期输出的影响。

LSTM(Hochreiter & Schmidhuber, Long short-term memory, Neural Computation 1997)是对这个问题的解答。

RNN 的隐藏状态打破了固定窗口。把同一组权重在时间轴上展开看,链条与瓶颈都在同一条线上。

RNN 按时间展开(每一步用同一组 W_x / W_h / b)

  x1       x2       x3       x4
   │        │        │        │
   ▼        ▼        ▼        ▼
┌────┐   ┌────┐   ┌────┐   ┌────┐
│ h1 │──▶│ h2 │──▶│ h3 │──▶│ h4 │──▶ …
└────┘   └────┘   └────┘   └────┘

窗口不再固定:h_t 理论上带着一路传下来的全部历史
参数也不随窗口增长:每步复用的都是同一组权重

瓶颈也在同一条链上,三条同时受限
  ① 计算:第 t 步必须等第 t−1 步 ──▶ 序列多长就串行多少步
  ② 梯度:反向传播沿这条链连乘 t 次 ──▶ 衰减或爆炸
  ③ 容量:整个历史压进固定维度的 h_t ──▶ 序列越长丢得越多

门控的机制 ​

核心是细胞状态(Cell State) Ct 加一套门控:

ft=σ(Wf[ht−1,xt]+bf)(遗忘门)it=σ(Wi[ht−1,xt]+bi)(输入门)C~t=tanh⁡(WC[ht−1,xt]+bC)(候选新信息)Ct=ft⊙Ct−1+it⊙C~tot=σ(Wo[ht−1,xt]+bo)(输出门)ht=ot⊙tanh⁡(Ct)
组件作用
细胞状态一条独立于隐藏状态的信息通路,让信息在时间步之间更顺畅地传递
遗忘门 ft决定从上一时刻的细胞状态里丢弃哪些信息
输入门 it决定把当前输入中的哪些新信息存入细胞状态
输出门 ot决定根据当前细胞状态输出哪些信息到隐藏状态

缓解梯度消失的关键在那条 Ct 的更新式:它是对上一时刻细胞状态的逐元素加权相加(不是矩阵乘加、也不是非线性变换)。这条加法通路让梯度可以沿它较无损地回传,因此常被形容为一条「常数误差传送带」。

ft 和 it 都是 sigmoid 输出,取值在 (0, 1)——这正是「门」的含义:它们控制信息通过的比重,可以学成「几乎全部保留」或「几乎全部丢弃」。

GRU 是后来的简化版本:把遗忘门和输入门合并成一个「更新门」,并去掉独立的细胞状态(用隐藏状态兼任),只保留更新门 + 重置门。参数更少、训练更快,在很多任务上与 LSTM 持平。

LSTM 的机制可以压缩成一张数据流图:一条加法通路加三扇门。

LSTM 单元:一条加法通路 + 三扇门

        C_{t−1}(上一刻的细胞状态)
           │
           ├──────────────────────────────┐
           │                              │
           ▼                              │
   ⊙ 遗忘门 f_t ──────────────────────────┤  丢弃哪些旧信息
                                          │
   ⊙ 输入门 i_t ⊗ 候选 C_cand ─────────────┤  存入哪些新信息
                                          │
                                          ▼
   C_t = f_t ⊙ C_{t−1} + i_t ⊙ C_cand        ← 逐元素加权相加
           │
           ▼
   ⊙ 输出门 o_t ──▶ h_t = o_t ⊙ tanh(C_t)    ← 交给下一步与下一层

三个门都是 sigmoid 输出,取值落在 (0, 1)——「门」的含义就是控制信息通过的比重,趋近 0 相当于全丢,趋近 1 相当于全留。

梯度沿哪条路回传,决定了梯度消失有没有被缓解:Ct 对 Ct−1 的依赖是逐元素缩放(乘 ft)加加法,不是权重矩阵连乘。所以这条通路上的梯度按 ft 缩放着往后传,衰减得比穿过 Wh 慢得多——这就是「常数误差传送带」这个形容的来处。

GRU 是简化版本:遗忘门与输入门合并成一个更新门,去掉独立的细胞状态,只留更新门与重置门。参数更少,很多任务上与 LSTM 持平。

RNN/LSTM 的瓶颈 ​

计算必须按顺序进行,第 t 步要等第 t−1 步完成。

这一条限制同时压住了三件事:

影响说明
无法大规模并行训练时无法把整个序列摊到多个设备上算
长序列效率低序列多长就要串行多少步
隐状态是固定长度的瓶颈整个历史必须压进一个固定维度的向量——序列越长,损失越多

第三个限制最根本,而且它不随门控改进而消失:LSTM 让信息传得更远,但**「所有历史压进定长向量」这个信息瓶颈没解决**。

这正是 02-Transformer 架构 要解决的问题:用注意力让任意两个位置直接相连,既绕开定长瓶颈,又把串行计算换成并行计算。

相关 ​

参考 ​

贡献者 ​

文件历史 ​